﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;

namespace SMplusXTeam.SyncMoped.SyncGurke
{
    /// <summary>
    /// This class provides some helper methods.
    /// </summary>
    public static class FileHelper
    {

        /// <summary>
        /// Finds recursively all files of a directory and its subdirectories.
        /// </summary>
        /// <param name="baseDirectory">The base directory.</param>
        /// <returns><see cref="IEnumerable{String}"/><para>All accessable files.</para></returns>
        public static IEnumerable<String> GetFiles(String baseDirectory)
        {
            if (String.IsNullOrEmpty(baseDirectory))
                throw new ArgumentNullException("baseDirectory");
            if (!Directory.Exists(baseDirectory))
                throw new ArgumentException("The following directory is not existing: " + baseDirectory);
            List<String> accessableFiles = new List<String>();
            ProcessAllFiles(baseDirectory, ProcessFile, accessableFiles);
            return accessableFiles;
        }

        private static void ProcessFile(String path, List<String> files)
        {
            files.Add(path);
        }

        private static void ProcessAllFiles(String folder, Action<String, List<String>> fileAction, List<String> accessableFiles)
        {
            try
            {
                foreach (String file in Directory.GetFiles(folder))
                {
                    fileAction(file, accessableFiles);
                }
            }
            catch (Exception ex)
            {
                TraceSource traceSource = new TraceSource(typeof(FileHelper).ToString());
                traceSource.TraceEvent(TraceEventType.Warning, 1, ex.Message);
                traceSource.Close();
            }
            try
            {
                foreach (string subDir in Directory.GetDirectories(folder))
                {
                    try
                    {
                        ProcessAllFiles(subDir, fileAction, accessableFiles);
                    }
                    catch (Exception ex)
                    {
                        TraceSource traceSource = new TraceSource(typeof(FileHelper).ToString());
                        traceSource.TraceEvent(TraceEventType.Warning, 1, ex.Message);
                        traceSource.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                TraceSource traceSource = new TraceSource(typeof(FileHelper).ToString());
                traceSource.TraceEvent(TraceEventType.Warning, 1, ex.Message);
                traceSource.Close();
            }
        }

        /// <summary>
        /// Creates a directory that is hidden.
        /// </summary>
        /// <param name="directory">The hidden directory that should be created.</param>
        public static void CreateHiddenFolder(String directory)
        {
            if (String.IsNullOrEmpty(directory))
                throw new ArgumentNullException("directory");
            DirectoryInfo dirInfo = null;
            if (!Directory.Exists(directory))
            {
                dirInfo = Directory.CreateDirectory(directory);
            }
            else
            {
                dirInfo = new DirectoryInfo(directory);
            }
            dirInfo.Attributes = FileAttributes.Directory | FileAttributes.Hidden;
        }

        /// <summary>
        /// Checks if one file is backed up.
        /// </summary>
        /// <param name="fileInfo1">The one file.</param>
        /// <param name="fileInfo2">The backup file.</param>
        /// <param name="comparisonMethod">The comparison method, i.e. MD5-Sum</param>
        /// <returns><see cref="bool"/><para>TRUE, if the files are backed up, otherwise FALSE.</para></returns>
        public static bool IsFileBackedUp(FileInfo fileInfo1, FileInfo fileInfo2, ComparisonMethod comparisonMethod)
        {
            bool result = true;
            if (comparisonMethod == ComparisonMethod.Md5)
            {
                if (fileInfo1.Length != fileInfo2.Length)
                    return false;
                byte[] firstHash = null;
                byte[] secondHash = null;
                using (FileStream fs = fileInfo1.OpenRead())
                {
                    firstHash = MD5.Create().ComputeHash(fs);
                }
                using (FileStream fs = fileInfo2.OpenRead())
                {
                    secondHash = MD5.Create().ComputeHash(fs);
                }
                if (!firstHash.SequenceEqual(secondHash))
                    return false;
            }
            else if (comparisonMethod == ComparisonMethod.Byte)
            {
                if (fileInfo1.Length != fileInfo2.Length)
                    return false;
                const int bufferLength = 1 * 1024 * 1024; //1MB
                using (FileStream fs1 = fileInfo1.OpenRead())
                using (FileStream fs2 = fileInfo2.OpenRead())
                using (BufferedStream bs1 = new BufferedStream(fs1))
                using (BufferedStream bs2 = new BufferedStream(fs2))
                {
                    long bytesRead = -1;
                    byte[] fileContent1 = new byte[bufferLength];
                    byte[] fileContent2 = new byte[bufferLength];
                    while (bytesRead != 0)
                    {
                        bytesRead = bs1.Read(fileContent1, 0, bufferLength);
                        bs2.Read(fileContent2, 0, bufferLength);
                        if (!fileContent1.SequenceEqual(fileContent2))
                        {
                            return false;
                        }
                    }
                }

            }
            else if (comparisonMethod == ComparisonMethod.ArchiveBit)
            {
                if ((fileInfo1.Attributes & FileAttributes.Archive) == FileAttributes.Archive)
                {
                    return false;
                }
            }
            else if (comparisonMethod == ComparisonMethod.Modified)
            {
                if (fileInfo1.LastWriteTimeUtc != fileInfo2.LastWriteTimeUtc)
                {
                    return false;
                }
            }
            return result;
        }

        /// <summary>
        /// Compares two files.
        /// </summary>
        /// <param name="sourceFile">The one file.</param>
        /// <param name="targetFile">The other file.</param>
        /// <param name="comparisonMethod">The comparison method, i.e. MD5-Sum</param>
        /// <returns><see cref="bool"/><para>TRUE, if the files are equal, otherwise FALSE.</para></returns>
        public static bool IsFileBackedUp(string sourceFile, string targetFile, ComparisonMethod comparisonMethod)
        {
            return IsFileBackedUp(new FileInfo(sourceFile), new FileInfo(targetFile), comparisonMethod);
        }

        /// <summary>
        /// Create a backup of a source file in the destination directory.
        /// When using versioned copy versions will be compressed and placed in
        /// versioning directory.
        /// </summary>
        /// <param name="sourceFile">File that needs backup.</param>
        /// <param name="destinationDir">Backup directory.</param>
        /// <param name="versions">Number of versions to keep.</param>
        /// <param name="versioningDir">Versioning directory.</param>
        public static void BackupFile(FileInfo sourceFile, DirectoryInfo destinationDir, int versions, DirectoryInfo versioningDir)
        {
            if (sourceFile == null)
                throw new ArgumentNullException("source");
            if (destinationDir == null)
                throw new ArgumentNullException("destination");
            if (versioningDir == null && versions != 0)
                throw new ArgumentNullException("versioningDirectory");

            if (!destinationDir.Exists)
                destinationDir.Create();

            string destFilePath = Path.Combine(destinationDir.FullName, sourceFile.Name);

            if (versions > 0)
            {
                if (!versioningDir.Exists)
                    versioningDir.Create();

                FileInfo destFile = new FileInfo(destFilePath);
                string versionFilePath = Path.Combine(versioningDir.FullName, destFile.Name) + "." + DateTime.Now.Ticks + ".gz";
                if (destFile.Exists)
                {
                    using (GZipStream zipStream = new GZipStream(File.OpenWrite(versionFilePath), CompressionMode.Compress))
                    {
                        using (FileStream destStream = destFile.OpenRead())
                        {
                            destStream.CopyTo(zipStream);
                        }
                    }
                }
            }

            sourceFile.CopyTo(destFilePath, true);
        }

        /// <summary>
        /// Create a backup of a source file in the destination directory.
        /// When using versioned copy versions will be compressed and placed in
        /// versioning directory.
        /// </summary>
        /// <param name="sourceFilePath">File that needs a backup.</param>
        /// <param name="destinationDirPath">Place to store backups.</param>
        /// <param name="versions">Number of versions to keep.</param>
        /// <param name="versioningDirPath">Versioning directory.</param>
        public static void BackupFile(string sourceFilePath, string destinationDirPath, int versions, string versioningDirPath)
        {
            FileInfo sourceFile = new FileInfo(sourceFilePath);
            DirectoryInfo destinationDir = new DirectoryInfo(destinationDirPath);
            DirectoryInfo versioningDir = new DirectoryInfo(versioningDirPath);

            BackupFile(sourceFile, destinationDir, versions, versioningDir);
        }

        public static string GetRelativePath(string childPath, string parentPath)
        {
            if (childPath.StartsWith(parentPath) && childPath.Length > parentPath.Length)
            {
                int parentLength = parentPath.Length;
                if (childPath[parentPath.Length] == Path.DirectorySeparatorChar)
                    parentLength++;
                return childPath.Substring(parentLength);
            }
            else
            {
                return childPath;
            }
        }

        /// <summary>
        /// Resolves the count of backed up files.
        /// </summary>
        /// <param name="filename">The name of the file.</param>
        /// <param name="backupPath">The backup path of the file.</param>
        /// <returns><see cref="uint"/><para>The count of backed up files, 0 means no backup.</para></returns>
        public static uint GetMaxVersion(String filename, String backupPath)
        {
            uint result = 0;
            String destFilePath = Path.Combine(backupPath, filename);
            for (int i = 1; i < Int32.MaxValue; i++)
            {
                String testedFullFile = destFilePath + "." + i;
                if (!File.Exists(testedFullFile))
                {
                    if (i == 1)
                    {
                        //return 0
                        break;
                    }
                    else
                    {
                        result = Convert.ToUInt32(i - 1);
                        break;
                    }
                }
            }
            return result;
        }

        /// <summary>
        /// Moves the backed up files for storing a new one (".1").
        /// </summary>
        /// <param name="filename">The name of the file.</param>
        /// <param name="backupPath">The backup path of the file.</param>
        /// <returns><see cref="uint"/><para>The count of backed up files, 0 means no backup.</para></returns>
        public static void MoveBackedUpFiles(String filename, String backupPath, int maxVersions)
        {
            String destFilePath = Path.Combine(backupPath, filename);
            uint latestVersion = GetMaxVersion(filename, backupPath);
            if (latestVersion > 0)
            {
                for (long i = latestVersion; i > 0; i--)
                {
                    String actualFullFile = destFilePath + "." + i;
                    if (File.Exists(actualFullFile))
                    {
                        if (i >= maxVersions)
                        {
                            File.Delete(actualFullFile);
                        }
                        else
                        {
                            File.Move(actualFullFile, destFilePath + "." + (i + 1));
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }

        }

        internal static void DeleteFile(string filePath, DirectoryInfo destinationDir, int versions, DirectoryInfo versioningDir)
        {
            if (filePath == null)
                throw new ArgumentNullException("file");
            if (destinationDir == null)
                throw new ArgumentNullException("destinationDir");
            if (versioningDir == null && versions != 0)
                throw new ArgumentNullException("versioningDirectory");

            if (!File.Exists(filePath))
                throw new ArgumentException("File " + filePath + " was not found in backup.");
            if (!destinationDir.Exists)
                throw new ArgumentException("Directory " + destinationDir.FullName + " does not exist.");

            if (!destinationDir.Exists)
                throw new ArgumentException("Directory " + destinationDir.FullName + " does not exist.");
        }
    }

    /// <summary>
    /// Declares the different comparison methods.
    /// </summary>
    public enum ComparisonMethod
    {
        ArchiveBit,
        Modified,
        Md5,
        Byte
    }
}
